package expo.modules.audio

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.media.audiofx.Visualizer
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import expo.modules.audio.service.AudioControlsService
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.sharedobjects.SharedRef
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID

private const val PLAYBACK_STATUS_UPDATE = "playbackStatusUpdate"
private const val AUDIO_SAMPLE_UPDATE = "audioSampleUpdate"
private const val SEEK_JUMP_INTERVAL_MS: Long = 10_000

@UnstableApi
class AudioPlayer(
  context: Context,
  appContext: AppContext,
  source: MediaSource?,
  private val updateInterval: Double
) : SharedRef<ExoPlayer>(
  ExoPlayer.Builder(context)
    .setLooper(context.mainLooper)
    .setAudioAttributes(AudioAttributes.DEFAULT, false)
    .setSeekForwardIncrementMs(SEEK_JUMP_INTERVAL_MS)
    .setSeekBackIncrementMs(SEEK_JUMP_INTERVAL_MS)
    .build(),
  appContext
) {
  val id = UUID.randomUUID().toString()
  var preservesPitch = true
  var isPaused = false
  var isMuted = false
  var previousVolume = 1f
  var onPlaybackStateChange: ((Boolean) -> Unit)? = null

  // Lock screen controls
  var isActiveForLockScreen = false
  private var metadata: Metadata? = null

  private var playerScope = CoroutineScope(Dispatchers.Default)
  private var samplingEnabled = false
  private var visualizer: Visualizer? = null
  private var playing = false
  private val context by lazy {
    appContext.reactContext
      ?: throw Exceptions.ReactContextLost()
  }

  private var updateJob: Job? = null

  val currentTime get() = ref.currentPosition / 1000f
  val duration get() = if (ref.duration != C.TIME_UNSET) ref.duration / 1000f else 0f

  init {
    addPlayerListeners()
    source?.let {
      setMediaSource(source)
    }
  }

  fun setVolume(volume: Float?) = appContext?.mainQueue?.launch {
    val boundedVolume = volume?.coerceIn(0f, 1f) ?: 1f
    if (isMuted) {
      if (boundedVolume > 0f) {
        previousVolume = boundedVolume
      }
      ref.volume = 0f
    } else {
      previousVolume = boundedVolume
      ref.volume = boundedVolume
    }
  }

  fun setMediaSource(source: MediaSource) {
    ref.setMediaSource(source)
    ref.prepare()
    startUpdating()
  }

  fun setActiveForLockScreen(active: Boolean, metadata: Metadata? = null, options: AudioLockScreenOptions? = null) {
    if (active) {
      this.metadata = metadata
      AudioControlsService.setActivePlayer(context, this, metadata, options)
    } else if (isActiveForLockScreen) {
      AudioControlsService.setActivePlayer(context, null)
    }
  }

  fun updateLockScreenMetadata(metadata: Metadata) {
    if (isActiveForLockScreen) {
      this.metadata = metadata
      AudioControlsService.updateMetadata(this, metadata)
    }
  }

  fun clearLockScreenControls() {
    if (isActiveForLockScreen) {
      AudioControlsService.setActivePlayer(context, null)
    }
  }

  private fun startUpdating() {
    updateJob?.cancel()
    updateJob = flow {
      while (true) {
        emit(Unit)
        delay(updateInterval.toLong())
      }
    }
      .onStart {
        sendPlayerUpdate()
      }
      .onEach {
        if (playing) {
          sendPlayerUpdate()
        }
      }
      .launchIn(playerScope)
  }

  private fun addPlayerListeners() = ref.addListener(object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
      playing = isPlaying
      playerScope.launch {
        sendPlayerUpdate(mapOf("playing" to isPlaying))
      }
      onPlaybackStateChange?.invoke(isPlaying)
    }

    override fun onIsLoadingChanged(isLoading: Boolean) {
      playerScope.launch {
        sendPlayerUpdate(mapOf("isLoaded" to !isLoading))
      }
    }

    override fun onPlaybackStateChanged(playbackState: Int) {
      playerScope.launch {
        sendPlayerUpdate(mapOf("playbackState" to playbackStateToString(playbackState)))
      }
    }

    override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
      playerScope.launch {
        sendPlayerUpdate()
      }
    }
  })

  fun setSamplingEnabled(enabled: Boolean) {
    appContext?.reactContext?.let {
      if (ContextCompat.checkSelfPermission(it, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
        Log.d(TAG, "\'android.permission.RECORD_AUDIO\' is required to use audio sampling. Please request this permission and try again.")
        return
      }
    }

    samplingEnabled = enabled
    if (enabled) {
      createVisualizer()
    } else {
      visualizer?.release()
      visualizer = null
    }
  }

  fun seekTo(seekTime: Double) {
    ref.seekTo((seekTime * 1000L).toLong())
    playerScope.launch {
      sendPlayerUpdate()
    }
  }

  private fun extractAmplitudes(chunk: ByteArray): List<Float> = chunk.map { byte ->
    val unsignedByte = byte.toInt() and 0xFF
    ((unsignedByte - 128).toDouble() / 128.0).toFloat()
  }

  fun currentStatus(): Map<String, Any?> {
    val isMuted = ref.volume == 0f
    val isLooping = ref.repeatMode == Player.REPEAT_MODE_ONE
    val isLoaded = ref.playbackState == Player.STATE_READY
    val isBuffering = ref.playbackState == Player.STATE_BUFFERING

    return mapOf(
      "id" to id,
      "currentTime" to currentTime,
      "playbackState" to playbackStateToString(ref.playbackState),
      "timeControlStatus" to if (ref.isPlaying) "playing" else "paused",
      "reasonForWaitingToPlay" to null,
      "mute" to isMuted,
      "duration" to duration,
      "playing" to ref.isPlaying,
      "loop" to isLooping,
      "didJustFinish" to (ref.playbackState == Player.STATE_ENDED),
      "isLoaded" to if (ref.playbackState == Player.STATE_ENDED) true else isLoaded,
      "playbackRate" to ref.playbackParameters.speed,
      "shouldCorrectPitch" to preservesPitch,
      "isBuffering" to isBuffering
    )
  }

  private suspend fun sendPlayerUpdate(map: Map<String, Any?>? = null) =
    withContext(Dispatchers.Main) {
      val data = currentStatus()
      val body = map?.let { data + it } ?: data
      emit(PLAYBACK_STATUS_UPDATE, body)
    }

  private fun sendAudioSampleUpdate(sample: List<Float>) {
    val body = mapOf(
      "channels" to listOf(
        mapOf("frames" to sample)
      ),
      "timestamp" to ref.currentPosition
    )
    emit(AUDIO_SAMPLE_UPDATE, body)
  }

  private fun playbackStateToString(state: Int): String {
    return when (state) {
      Player.STATE_READY -> "ready"
      Player.STATE_BUFFERING -> "buffering"
      Player.STATE_IDLE -> "idle"
      Player.STATE_ENDED -> "ended"
      else -> "unknown"
    }
  }

  private fun createVisualizer() {
    // It must only be created once, otherwise the app will crash
    if (visualizer == null) {
      visualizer = Visualizer(ref.audioSessionId).apply {
        captureSize = Visualizer.getCaptureSizeRange()[1]
        setDataCaptureListener(
          object : Visualizer.OnDataCaptureListener {
            override fun onWaveFormDataCapture(visualizer: Visualizer?, waveform: ByteArray?, samplingRate: Int) {
              waveform?.let {
                if (samplingEnabled && ref.isPlaying) {
                  val data = extractAmplitudes(it)
                  sendAudioSampleUpdate(data)
                }
              }
            }

            override fun onFftDataCapture(visualizer: Visualizer?, fft: ByteArray?, samplingRate: Int) = Unit
          },
          Visualizer.getMaxCaptureRate() / 2,
          true,
          false
        )
        enabled = true
      }
    }
  }

  override fun sharedObjectDidRelease() {
    super.sharedObjectDidRelease()
    appContext?.mainQueue?.launch {
      if (isActiveForLockScreen) {
        AudioControlsService.clearSession()
      }
      playerScope.cancel()
      visualizer?.release()
      ref.release()
    }
  }

  companion object {
    val TAG = AudioPlayer::class.simpleName
  }
}
